package edu.gatech.cs4220;

import java.util.Collection;
import java.util.Map;
import android.location.Location;
import java.util.SortedSet;

//FIXME add serialization
public class LocationProfileSet implements java.io.Serializable, Cloneable
{
	private static final long serialVersionUID = -8084071864774286678L;
	
	static final String DEFAULT_PROFILE_NAME = "[DEFAULT]";

	/**
	 * Whatever implementation of Map this is, it must be that when methods iterate over it, another thread's add request will not invalidate it.
	 * This may be done by copy on write or locking.
	 * This implementation uses copy on write.
	 */
	private Map<String, LocationProfile> profiles;
	
	//XXX make a private member
	private volatile LocationProfile currentProfile;
	
	/** This is the default profile that is marked for application when no other eligble profile can be found.
	 * Note that this profile can be marked for application even if it is not elegible otherwise.
	 */
	private LocationProfile defaultProfile;
	
	public LocationProfileSet(LocationProfile defaultProfile)
	{
		if(defaultProfile == null)
			throw new NullPointerException("Cannot have a null default profile");
		profiles = new java.util.concurrent.ConcurrentHashMap<String, LocationProfile>();
		this.defaultProfile = defaultProfile;
		profiles.put(DEFAULT_PROFILE_NAME, defaultProfile);
	}
	
	public LocationProfileSet(java.util.Collection<? extends LocationProfile> coll, LocationProfile defaultProfile)
	{
		if(defaultProfile == null)
			throw new NullPointerException("Cannot have a null default profile");
		this.defaultProfile = defaultProfile;
		profiles = new java.util.concurrent.ConcurrentHashMap<String, LocationProfile>(coll.size());
		profiles.put(DEFAULT_PROFILE_NAME, defaultProfile);
		for(LocationProfile profile : coll)
		{
			profiles.put(profile.getName(), profile);
		}
	}
	
	void applyProfile(String profileName)
	{
		currentProfile = profiles.get(profileName);
		currentProfile.settings.apply();
	}
	
	void applyProfile(LocationProfile profile)
	{
		//Do not apply if this profile is not in this set
		if(!containsExactProfile(profile))
			return;
		currentProfile = profile;
		profile.settings.apply();
	}
	
	void applyDefaultProfile()
	{
		currentProfile = defaultProfile;
		defaultProfile.settings.apply();
	}
	
	LocationProfile getCurrentProfile()
	{
		return currentProfile;
	}
	
	public static final LocationProfileSet generateBlankSet(android.content.Context APP_CONTEXT)
	{
		return new LocationProfileSet(new LocationProfile(APP_CONTEXT, DEFAULT_PROFILE_NAME));
	}
	
	public boolean containsProfileName(String name)
	{
		return profiles.containsKey(name);
	}
	
	public boolean containsExactProfile(LocationProfile profile)
	{
		return profiles.containsValue(profile);
	}
	
	public LocationProfile getProfile(String name)
	{
		return profiles.get(name);
	}
	
	public boolean renameProfile(String oldName, String newName)
	{
		if(!containsProfileName(oldName))
			return false;
		if(containsProfileName(newName))
			return false;
		LocationProfile oldProfile = profiles.get(oldName);
		LocationProfile newProfile = oldProfile.getNewWithChangedName(newName);
		profiles.put(newName, newProfile);
		profiles.remove(oldName);
		return true;
	}
	
	public LocationProfile addProfile(LocationProfile profile)
	{
		return profiles.put(profile.getName(), profile);
	}
	
	public LocationProfile removeProfile(String name)
	{
		return profiles.remove(name);
	}
	
	LocationProfile getClosestElegibleProfile(LocationProfilePoint location)
	{
		SortedSet<LocationProfile> profilesSorted = new java.util.TreeSet<LocationProfile>(
				new LocationProfileToPointComparator(location));
		//First, sort out profiles that don't match the location
		for(LocationProfile profile : profiles.values())
		{
			if(profile.quickIsElegible() && profile.isLocationInProfile(location))
				profilesSorted.add(profile);	
		}
		//Get the closest, or the default if none found
		return profilesSorted.size() == 0 ? defaultProfile : profilesSorted.first();
	}
	
	LocationProfile getClosestElegibleProfile(Location l)
	{
		return getClosestElegibleProfile(new LocationProfilePoint(l));
	}
	
	Collection<LocationProfile> getProfiles()
	{
		return profiles.values();
	}
	
	Collection<LocationProfile> getProfilesForDisplay()
	{
		Collection<LocationProfile> result = new java.util.ArrayList<LocationProfile>(getProfiles());
		result.remove(defaultProfile);
		return result;
	}

	public LocationProfile getDefaultProfile()
	{
		return defaultProfile;
	}
	
	private static class LocationProfileToPointComparator implements java.util.Comparator<LocationProfile>
	{
		private LocationProfilePoint locationBase;
		
		LocationProfileToPointComparator(LocationProfilePoint locationBase)
		{
			this.locationBase = locationBase;
		}
		
		public int compare(LocationProfile l1, LocationProfile l2)
		{
			//Store these in variables as we don't want to call this function several times
			float l1Dist = l1.getClosestDistanceToLocation(locationBase);
			float l2Dist = l2.getClosestDistanceToLocation(locationBase);
			if(l1Dist < l2Dist)
				return -1;
			if(l1Dist > l2Dist)
				return 1;
			return 0;
		}
	}
	
	@Override
	public LocationProfileSet clone()
	{
		LocationProfileSet result = null;
		try
		{
			result = (LocationProfileSet)super.clone();
		}
		catch(CloneNotSupportedException err)
		{
			geoprofiles.logErrorAndThrowAssertion("LocationProfileSet is cloneable", err);
		}
		
		return result;
	}
	
}
